using System;
using EarthGeospatial;
using Geometry;
using Unity.Mathematics;

namespace Cesium3DTilesSelection
{
    public class RasterMappedTo3DTile
    {
        public enum AttachmentState
        {
            /**
             * @brief This raster tile is not yet attached to the geometry at all.
             */
            Unattached = 0,

            /**
             * @brief This raster tile is attached to the geometry, but it is a
             * temporary, low-res version usable while the full-res version is loading.
             */
            TemporarilyAttached = 1,

            /**
             * @brief This raster tile is attached to the geometry.
             */
            Attached = 2
        };

        public RasterOverlayTile pLoadingTile { get; set; }
        public RasterOverlayTile pReadyTile { get; set; }

        private int _textureCoordinateID;
        private double2 _translation;
        private double2 _scale;
        private AttachmentState _state;
        private bool _originalFailed;
        public RasterMappedTo3DTile(RasterOverlayTile pRasterTile)
        {
            this.pLoadingTile = pRasterTile;
            this.pReadyTile = null;
            _textureCoordinateID = 0;
            _translation = double2.zero;
            _scale = new double2(1, 1);
            _state = AttachmentState.Unattached;
            _originalFailed = false;
        }
        public void setTextureCoordinateID(int textureCoordinateID)
        {
            this._textureCoordinateID = textureCoordinateID;
        }

        public RasterOverlayTile.MoreDetailAvailable update(Tile tile)
        {
            if (this._state == AttachmentState.Attached)
            {
                return !this._originalFailed && this.pReadyTile && this.pReadyTile.moreDetailAvailable != RasterOverlayTile.MoreDetailAvailable.No
                           ? RasterOverlayTile.MoreDetailAvailable.Yes
                           : RasterOverlayTile.MoreDetailAvailable.No;
            }

            // If the loading tile has failed, try its parent's loading tile.
            Tile pTile = tile;
            while (this.pLoadingTile && (this.pLoadingTile.State == RasterOverlayTile.LoadState.Failed) && pTile)
            {
                // Note when our original tile fails to load so that we don't report more
                // data available. This means - by design - we won't refine past a failed
                // tile.
                this._originalFailed = true;

                pTile = pTile.pParent;
                if (pTile)
                {
                    RasterOverlayTile pOverlayTile = findTileOverlay(pTile, this.pLoadingTile.pOverlay);
                    if (pOverlayTile)
                    {
                        this.pLoadingTile = pOverlayTile;
                    }
                }
            }

            // If the loading tile is now ready, make it the ready tile.
            if (this.pLoadingTile && this.pLoadingTile.State >= RasterOverlayTile.LoadState.Loaded)
            {
                // Unattach the old tile
                if (this.pReadyTile && this._state != AttachmentState.Unattached)
                {
                    TilesetExternals externals = tile.pTileset.externals;
                    externals.pPrepareRendererResources.detachRasterInMainThread(tile,
                        this._textureCoordinateID,
                        this.pReadyTile,
                        this.pReadyTile.pRendererResources);
                    this._state = AttachmentState.Unattached;
                }

                // Mark the loading tile ready.
                this.pReadyTile = this.pLoadingTile;
                this.pLoadingTile = null;

                // Compute the translation and scale for the new tile.
                this.computeTranslationAndScale(tile);
            }

            // Find the closest ready ancestor tile.
            if (this.pLoadingTile)
            {
                RasterOverlayTile pCandidate = null;

                pTile = tile.pParent;
                while (pTile)
                {
                    pCandidate = findTileOverlay(pTile, this.pLoadingTile.pOverlay);
                    if (pCandidate &&
                        pCandidate.State >= RasterOverlayTile.LoadState.Loaded)
                    {
                        break;
                    }
                    pTile = pTile.pParent;
                }

                if (pCandidate &&
                    pCandidate.State >= RasterOverlayTile.LoadState.Loaded &&
                    this.pReadyTile != pCandidate)
                {
                    if (this._state != AttachmentState.Unattached)
                    {
                        TilesetExternals externals = tile.pTileset.externals;
                        externals.pPrepareRendererResources.detachRasterInMainThread(
                            tile,
                            this._textureCoordinateID,
                            this.pReadyTile,
                            this.pReadyTile.pRendererResources);
                        this._state = AttachmentState.Unattached;
                    }

                    this.pReadyTile = pCandidate;

                    // Compute the translation and scale for the new tile.
                    this.computeTranslationAndScale(tile);
                }
            }

            // Attach the ready tile if it's not already attached.
            if (this.pReadyTile && this._state == RasterMappedTo3DTile.AttachmentState.Unattached)
            {
                this.pReadyTile.loadInMainThread();
                TilesetExternals externals = tile.pTileset.externals;
                externals.pPrepareRendererResources.attachRasterInMainThread(
                    tile,
                    this._textureCoordinateID,
                    this.pReadyTile,
                    this.pReadyTile.pRendererResources,
                    this._translation,
                    this._scale);

                this._state = this.pLoadingTile ? AttachmentState.TemporarilyAttached : AttachmentState.Attached;

                CesiumTextureUtility.saveImage("cc.jpg",this.pReadyTile.pRendererResources as UnityEngine.Texture2D);
            }

            // TODO: check more precise raster overlay tile availability, rather than just
            // max level?
            if (this.pLoadingTile)
            {
                return RasterOverlayTile.MoreDetailAvailable.Unknown;
            }

            if (!this._originalFailed && this.pReadyTile)
            {
                return this.pReadyTile.moreDetailAvailable;
            }
            else
            {
                return RasterOverlayTile.MoreDetailAvailable.No;
            }
        }

        private void computeTranslationAndScale(Tile tile)
        {
            if (!this.pReadyTile)
            {
                return;
            }
            GlobeRectangle pRectangle = Impl.obtainGlobeRectangle(tile.boundingVolume);
            if (!pRectangle)
            {
                return;
            }

            RasterOverlayTileProvider tileProvider = this.pReadyTile.pOverlay.getTileProvider();
            Rectangle geometryRectangle = Projection.projectRectangleSimple(tileProvider.pProjection, pRectangle);
            Rectangle imageryRectangle = this.pReadyTile.Rectangle;

            double terrainWidth = geometryRectangle.computeWidth();
            double terrainHeight = geometryRectangle.computeHeight();

            double scaleX = terrainWidth / imageryRectangle.computeWidth();
            double scaleY = terrainHeight / imageryRectangle.computeHeight();
            this._translation = new double2(
                (scaleX * (geometryRectangle.minimumX - imageryRectangle.minimumX)) /
                    terrainWidth,
                (scaleY * (geometryRectangle.minimumY - imageryRectangle.minimumY)) /
                    terrainHeight);
            this._scale = new double2(scaleX, scaleY);
        }

        private RasterOverlayTile findTileOverlay(Tile pTile, RasterOverlay pOverlay)
        {
            var tiles = pTile.rasterTiles;
            var mapped = tiles.Find(x => x.pReadyTile != null && x.pReadyTile.pOverlay == pOverlay);
            if (mapped != null)
            {
                if (mapped.pLoadingTile)
                {
                    return mapped.pLoadingTile;
                }
                else
                {
                    return mapped.pReadyTile;
                }
            }
            return null;
        }
    }
}